﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Media;

namespace CokoChip8.Engine
{
    public class Chip8Cpu
    {
        public byte[] RAM = new byte[4096];                     // Mémoire principale
        public ushort PC = 0x200;                               // Compteur 12 bits, positionné a 0x200 de base (les programmes ne peuvent pas utiliser l'espace entre 0 et 0x200)

        public byte[] V = new byte[16];                         // 16 registres V de 8 bits
        public ushort I;                                        // Registre I de 16 bits (utilisé pour stocker des valeurs de la PC en général)

        public Stack<ushort> Stack = new Stack<ushort>(16);     // Pile des appels

        private byte[] _ScreenBuffer = new byte[8192];          // Buffer de l'image

        // Timers 8bit
        private byte _DT = 0;                                   // Timer général
        public byte _ST = 0;                                    // Timer du son

        private Random RandGenerator = new Random();

        public bool[] Keyboard = new bool[16];


        public Chip8Cpu()
        {
            RAM[0] = 240; RAM[1] = 144; RAM[2] = 144; RAM[3] = 144; RAM[4] = 240;
            RAM[5] = 32; RAM[6] = 96; RAM[7] = 32; RAM[8] = 32; RAM[9] = 112;
            RAM[10] = 240; RAM[11] = 16; RAM[12] = 240; RAM[13] = 128; RAM[14] = 240;
            RAM[15] = 240; RAM[16] = 16; RAM[17] = 240; RAM[18] = 16; RAM[19] = 240;
            RAM[20] = 144; RAM[21] = 144; RAM[22] = 240; RAM[23] = 16; RAM[24] = 16;
            RAM[25] = 240; RAM[26] = 128; RAM[27] = 240; RAM[28] = 16; RAM[29] = 240;
            RAM[30] = 240; RAM[31] = 128; RAM[32] = 240; RAM[33] = 144; RAM[34] = 240;
            RAM[35] = 240; RAM[36] = 16; RAM[37] = 32; RAM[38] = 64; RAM[39] = 64;
            RAM[40] = 240; RAM[41] = 144; RAM[42] = 240; RAM[43] = 144; RAM[44] = 240;
            RAM[45] = 240; RAM[46] = 144; RAM[47] = 240; RAM[48] = 16; RAM[49] = 240;
            RAM[50] = 240; RAM[51] = 144; RAM[52] = 240; RAM[53] = 144; RAM[54] = 144;
            RAM[55] = 224; RAM[56] = 144; RAM[57] = 224; RAM[58] = 144; RAM[59] = 224;
            RAM[60] = 240; RAM[61] = 128; RAM[62] = 128; RAM[63] = 128; RAM[64] = 240;
            RAM[65] = 224; RAM[66] = 144; RAM[67] = 144; RAM[68] = 144; RAM[69] = 224;
            RAM[70] = 240; RAM[71] = 128; RAM[72] = 240; RAM[73] = 128; RAM[74] = 240;
            RAM[75] = 240; RAM[76] = 128; RAM[77] = 240; RAM[78] = 128; RAM[79] = 128;
        }

        public byte[] GetScreenBuffer()
        {
            lock (_ScreenBuffer)
            {
                return _ScreenBuffer;
            }
        }

        public void Load(byte[] rom)
        {
            Reset();

            // On charge notre ROM en RAM (à partir de l'adresse 0x200)
            for (var i = 0; i < rom.Length; i++)
            {
                RAM[0x200 + i] = rom[i];
            }

            Emulate();
        }

        public void Reset()
        {
            // On vide la pile d'appel
            Stack.Clear();
            // On reset PC
            PC = 0x200;
            // Et l'écran
            Inst_00E0();
        }

        public void Emulate()
        {
            // Récupération de l'instruction pointée par le PC
            ushort instruction = (ushort)((RAM[PC] << 8) + RAM[PC + 1]);

            PC = (ushort)(PC + 2);

            var firstCode = (byte)((instruction & 0xF000) >> 12);
            var nnn = (ushort)(instruction & 0x0FFF);
            var x = (byte)((instruction & 0x0F00) >> 8);
            var y = (byte)((instruction & 0x00F0) >> 4);
            var kk = (byte)(instruction & 0x00FF);
            var k = (byte)(instruction & 0x000F);

            switch (firstCode)
            {
                case 0x0:
                    {
                        switch (instruction)
                        {
                            case 0x00E0: Inst_00E0(); break;
                            case 0x00EE: Inst_00EE(); break;
                        }
                        break;
                    }
                case 0x1:
                    {
                        Inst_1NNN(nnn);
                        break;
                    }
                case 0x2:
                    {
                        Inst_2NNN(nnn);
                        break;
                    }
                case 0x3:
                    {
                        Inst_3XKK(x, kk);
                        break;
                    }
                case 0x4:
                    {
                        Inst_3XKK(x, kk);
                        break;
                    }
                case 0x5:
                    {
                        Inst_5XY0(x, y);
                        break;
                    }
                case 0x6:
                    {
                        Inst_6XKK(x, kk);
                        break;
                    }
                case 0x7:
                    {
                        Inst_7XKK(x, kk);
                        break;
                    }
                case 0x8:
                    {
                        switch (k)
                        {
                            case 0x0: Inst_8XY0(x, y); break;
                            case 0x1: Inst_8XY1(x, y); break;
                            case 0x2: Inst_8XY2(x, y); break;
                            case 0x3: Inst_8XY3(x, y); break;
                            case 0x4: Inst_8XY4(x, y); break;
                            case 0x5: Inst_8XY5(x, y); break;
                            case 0x6: Inst_8XY6(x, y); break;
                            case 0x7: Inst_8XY7(x, y); break;
                            case 0xE: Inst_8XYE(x, y); break;
                        }
                        break;
                    }
                case 0x9:
                    {
                        Inst_9XY0(x, y);
                        break;
                    }
                case 0xA:
                    {
                        Inst_ANNN(nnn);
                        break;
                    }
                case 0xB:
                    {
                        Inst_BNNN(nnn);
                        break;
                    }
                case 0xC:
                    {
                        Inst_CXKK(x, kk);
                        break;
                    }
                case 0xD:
                    {
                        Inst_DXYN(x, y, k);
                        break;
                    }
                case 0xE:
                    {
                        switch(kk){
                            case 0x9E: Inst_EX9E(x); break;
                            case 0xA1: Inst_EXA1(x); break;
                        }
                        break;
                    }
                case 0xF:
                    {
                        switch (kk)
                        {
                            case 0x07: Inst_FX07(x); break;
                            case 0x0A: Inst_FX0A(x); break;
                            case 0x15: Inst_FX15(x); break;
                            case 0x18: Inst_FX18(x); break;
                            case 0x1E: Inst_FX1E(x); break;
                            case 0x29: Inst_FX29(x); break;
                            case 0x33: Inst_FX33(x); break;
                            case 0x55: Inst_FX55(x); break;
                            case 0x65: Inst_FX65(x); break;
                        }
                        break;
                    }
            }

            // On décrémente les timers de 1 si ils sont > 0
            if (_DT > 0)
                _DT--;
            if (_ST > 0)
                _ST--;
        }


        /// <summary>
        /// 00E0 - CLS
        /// Clear the display.
        /// </summary>
        private void Inst_00E0()
        {
            // noir = {B = 0, G = 0, R = 0, A = 255}
            for (int i = 0; i < _ScreenBuffer.Length; i += 4)
            {
                _ScreenBuffer[i] = 0;
                _ScreenBuffer[i + 1] = 0;
                _ScreenBuffer[i + 2] = 0;
                _ScreenBuffer[i + 3] = 255;
            }
        }

        /// <summary>
        /// 00EE - RET
        /// Return from a subroutine.
        /// The interpreter sets the program counter to the address at the top of the stack, then subtracts 1 from the stack pointer.
        /// </summary>
        private void Inst_00EE()
        {
            PC = this.Stack.Pop();
        }

        /// <summary>
        /// 1nnn - JP addr
        /// Jump to location nnn.
        /// The interpreter sets the program counter to nnn.
        /// </summary>
        private void Inst_1NNN(ushort NNN)
        {
            PC = NNN;
        }


        /// <summary>
        /// 2nnn - CALL addr
        /// Call subroutine at nnn.
        /// The interpreter increments the stack pointer, then puts the current PC on the top of the stack. The PC is then set to nnn.
        /// </summary>
        private void Inst_2NNN(ushort NNN)
        {
            this.Stack.Push(PC);
            PC = NNN;
        }

        /// <summary>
        /// 3xkk - SE Vx, byte
        /// Skip next instruction if Vx = kk.
        /// The interpreter compares register Vx to kk, and if they are equal, increments the program counter by 2.
        /// </summary>
        private void Inst_3XKK(byte x, byte kk)
        {
            if (this.V[x] == kk)
            {
                PC+=2;
            }
        }

        /// <summary>
        /// 4xkk - SNE Vx, byte
        /// Skip next instruction if Vx != kk.
        /// The interpreter compares register Vx to kk, and if they are not equal, increments the program counter by 2.
        /// </summary>
        private void Inst_4XKK(byte x, byte kk)
        {
            if (this.V[x] != kk)
            {
                PC += 2;
            }
        }

        /// <summary>
        /// 5xy0 - SE Vx, Vy
        /// Skip next instruction if Vx = Vy.
        /// The interpreter compares register Vx to register Vy, and if they are equal, increments the program counter by 2.
        /// </summary>
        private void Inst_5XY0(byte x, byte y)
        {
            if (this.V[x] == this.V[y])
            {
                PC += 2;
            }
        }

        /// <summary>
        /// 6xkk - LD Vx, byte
        /// Set Vx = kk.
        /// The interpreter puts the value kk into register Vx.
        /// </summary>
        private void Inst_6XKK(byte x, byte kk)
        {
            this.V[x] = kk;
        }

        /// <summary>
        /// 7xkk - ADD Vx, byte
        /// Set Vx = Vx + kk.
        /// Adds the value kk to the value of register Vx, then stores the result in Vx.
        /// </summary>
        private void Inst_7XKK(byte x, byte kk)
        {
            this.V[x] = (byte)(this.V[x] + kk);
        }


        /// <summary>
        /// 8xy0 - LD Vx, Vy
        /// Set Vx = Vy.
        /// Stores the value of register Vy in register Vx.
        /// </summary>
        private void Inst_8XY0(byte x, byte y)
        {
            this.V[x] = this.V[y];
        }

        /// <summary>
        /// 8xy1 - OR Vx, Vy
        /// Set Vx = Vx OR Vy.
        /// Performs a bitwise OR on the values of Vx and Vy, then stores the result in Vx. A bitwise OR compares the corrseponding bits from two values, and if either bit is 1, then the same bit in the result is also 1. Otherwise, it is 0.
        /// </summary>
        private void Inst_8XY1(byte x, byte y)
        {
            this.V[x] = (byte)(this.V[x] | this.V[y]);
        }

        /// <summary>
        /// 8xy2 - AND Vx, Vy
        /// Set Vx = Vx AND Vy.
        /// Performs a bitwise AND on the values of Vx and Vy, then stores the result in Vx. A bitwise AND compares the corrseponding bits from two values, and if both bits are 1, then the same bit in the result is also 1. Otherwise, it is 0.
        /// </summary>
        private void Inst_8XY2(byte x, byte y)
        {
            this.V[x] = (byte)(this.V[x] & this.V[y]);
        }

        /// <summary>
        /// 8xy3 - XOR Vx, Vy
        /// Set Vx = Vx XOR Vy.
        /// Performs a bitwise exclusive OR on the values of Vx and Vy, then stores the result in Vx. An exclusive OR compares the corrseponding bits from two values, and if the bits are not both the same, then the corresponding bit in the result is set to 1. Otherwise, it is 0.
        /// </summary>
        private void Inst_8XY3(byte x, byte y)
        {
            this.V[x] = (byte)(this.V[x] ^ this.V[y]);
        }

        /// <summary>
        /// 8xy4 - ADD Vx, Vy
        /// Set Vx = Vx + Vy, set VF = carry.
        /// The values of Vx and Vy are added together. If the result is greater than 8 bits (i.e., > 255,) VF is set to 1, otherwise 0. Only the lowest 8 bits of the result are kept, and stored in Vx.
        /// </summary>
        private void Inst_8XY4(byte x, byte y)
        {
            ushort val = (ushort)(this.V[x] + this.V[y]);

            if (val > 0xFF)
            {
                this.V[0xF] = 1;
            }
            else
            {
                this.V[0xF] = 0;
            }

            this.V[x] = (byte)(val & 0x00FF);
        }

        /// <summary>
        /// 8xy5 - SUB Vx, Vy
        /// Set Vx = Vx - Vy, set VF = NOT borrow.
        /// If Vx > Vy, then VF is set to 1, otherwise 0. Then Vy is subtracted from Vx, and the results stored in Vx.
        /// </summary>
        private void Inst_8XY5(byte x, byte y)
        {
            if (this.V[x] > this.V[y])
            {
                this.V[0xF] = 1;
            }
            else
            {
                this.V[0xF] = 0;
            }

            this.V[x] = (byte)(this.V[x] - this.V[y]); 

        }

        /// <summary>
        /// 8xy6 - SHR Vx {, Vy}
        /// Set Vx = Vx SHR 1.
        /// If the least-significant bit of Vx is 1, then VF is set to 1, otherwise 0. Then Vx is divided by 2.
        /// </summary>
        private void Inst_8XY6(byte x, byte y)
        {
            if ((this.V[x] & 0x0F) == 1)
            {
                this.V[0xF] = 1;
            }
            else
            {
                this.V[0xF] = 0;
            }

            this.V[x] /= 2;
        }

        /// <summary>
        /// 8xy7 - SUBN Vx, Vy
        /// Set Vx = Vy - Vx, set VF = NOT borrow.
        /// If Vy > Vx, then VF is set to 1, otherwise 0. Then Vx is subtracted from Vy, and the results stored in Vx.
        /// </summary>
        private void Inst_8XY7(byte x, byte y)
        {
            if (this.V[y] > this.V[x])
            {
                this.V[0xF] = 1;
            }
            else
            {
                this.V[0xF] = 0;
            }

            this.V[x] = (byte)(this.V[y] - this.V[x]); 
        }

        /// <summary>
        /// 8xyE - SHL Vx {, Vy}
        /// Set Vx = Vx SHL 1.
        /// If the most-significant bit of Vx is 1, then VF is set to 1, otherwise to 0. Then Vx is multiplied by 2.
        /// </summary>
        private void Inst_8XYE(byte x, byte y)
        {
            if ((this.V[x] & 0xF0) == 1)
            {
                this.V[0xF] = 1;
            }
            else
            {
                this.V[0xF] = 0;
            }

            this.V[x] *= 2;
        }

        /// <summary>
        /// 9xy0 - SNE Vx, Vy
        /// Skip next instruction if Vx != Vy.
        /// The values of Vx and Vy are compared, and if they are not equal, the program counter is increased by 2.
        /// </summary>
        private void Inst_9XY0(byte x, byte y)
        {
            if (this.V[x] != this.V[y])
            {
                this.PC += 2;
            }
        }

        /// <summary>
        /// Annn - LD I, addr
        /// Set I = nnn.
        /// The value of register I is set to nnn.
        /// </summary>
        private void Inst_ANNN(ushort nnn)
        {
            this.I = nnn;
        }


        /// <summary>
        /// Bnnn - JP V0, addr
        /// Jump to location nnn + V0.
        /// The program counter is set to nnn plus the value of V0.
        /// </summary>
        private void Inst_BNNN(ushort nnn)
        {
            PC = (byte)(nnn + this.V[0]);
        }


        /// <summary>
        /// Cxkk - RND Vx, byte
        /// Set Vx = random byte AND kk.
        /// The interpreter generates a random number from 0 to 255, which is then ANDed with the value kk. The results are stored in Vx. See instruction 8xy2 for more information on AND.
        /// </summary>
        private void Inst_CXKK(byte x, byte kk)
        {
            this.V[x] = (byte)((byte)RandGenerator.Next(0, 256) & kk);
        }

        /// <summary>
        /// Dxyn - DRW Vx, Vy, nibble
        /// Display n-byte sprite starting at memory location I at (Vx, Vy), set VF = collision.
        /// The interpreter reads n bytes from memory, starting at the address stored in I. These bytes are then displayed as sprites on screen at coordinates (Vx, Vy). Sprites are XORed onto the existing screen. If this causes any pixels to be erased, VF is set to 1, otherwise it is set to 0. If the sprite is positioned so part of it is outside the coordinates of the display, it wraps around to the opposite side of the screen. See instruction 8xy3 for more information on XOR, and section 2.4, Display, for more information on the Chip-8 screen and sprites.
        /// </summary>
        private void Inst_DXYN(byte x, byte y, byte k)
        {
            this.V[0xF] = 0;

            for(var i = 0; i < k ; i++)
            {
                byte ligne = RAM[I + i];

                // Pour chaque pixel dans la ligne
                for (var n = 0; n < 8; n++)
                {
                    // On extrait le pixel à écrire
                    byte pixel = (byte)((byte)(ligne << n) >> 7);

                    // On calcul l'adresse du pixel dans le frame buffer
                    int pixelAddress = V[x] + n + (V[y] + i) * 64;
                    pixelAddress *= 4;
                    pixelAddress %= _ScreenBuffer.Length;

                    // On fait la somme des composantes, pour savoir si il y a une couleur
                    int couleur = _ScreenBuffer[pixelAddress] + _ScreenBuffer[pixelAddress + 1] + _ScreenBuffer[pixelAddress + 2];

                    // On l'écrit en XOR
                    lock (_ScreenBuffer)
                    {
                        if ((pixel != 0 && couleur == 0) || (pixel == 0 && couleur != 0))
                        {
                            _ScreenBuffer[pixelAddress] = 255;
                            _ScreenBuffer[pixelAddress + 1] = 255;
                            _ScreenBuffer[pixelAddress + 2] = 255;
                            _ScreenBuffer[pixelAddress + 3] = 255;
                        }
                        else
                        {
                            _ScreenBuffer[pixelAddress] = 0;
                            _ScreenBuffer[pixelAddress + 1] = 0;
                            _ScreenBuffer[pixelAddress + 2] = 0;
                            _ScreenBuffer[pixelAddress + 3] = 255;
                        }
                    }

                    // Et on détecte la collision
                    if (pixel != 0 && couleur != 0)
                        V[0xF] = 1;
                }
            }
        }

        /// <summary>
        /// Ex9E - SKP Vx
        /// Skip next instruction if key with the value of Vx is pressed.
        /// Checks the keyboard, and if the key corresponding to the value of Vx is currently in the down position, PC is increased by 2.
        /// </summary>
        private void Inst_EX9E(byte x)
        {
            if (this.Keyboard[this.V[x]])
            {
                this.PC += 2;
            }
        }

        /// <summary>
        /// ExA1 - SKNP Vx
        /// Skip next instruction if key with the value of Vx is not pressed.
        /// Checks the keyboard, and if the key corresponding to the value of Vx is currently in the up position, PC is increased by 2.
        /// </summary>
        private void Inst_EXA1(byte x)
        {
            if (!this.Keyboard[this.V[x]])
            {
                this.PC += 2;
            }
        }


        /// <summary>
        /// Fx07 - LD Vx, DT
        /// Set Vx = delay timer value.
        /// The value of DT is placed into Vx.
        /// </summary>
        private void Inst_FX07(byte x)
        {
            this.V[x] = this._DT;
        }


        /// <summary>
        /// Fx0A - LD Vx, K
        /// Wait for a key press, store the value of the key in Vx.
        /// All execution stops until a key is pressed, then the value of that key is stored in Vx.
        /// </summary>
        private void Inst_FX0A(byte x)
        {
            while (true)
            {
                for (byte i = 0; i < 16; i++)
                {
                    if (Keyboard[i] == true)
                    {
                        this.V[x] = i;
                        return;
                    }
                }
            }  
        }


        /// <summary>
        /// Fx15 - LD DT, Vx
        /// Set delay timer = Vx.
        /// DT is set equal to the value of Vx.
        /// </summary>
        private void Inst_FX15(byte x)
        {
            this._DT = this.V[x];
        }


        /// <summary>
        /// Fx18 - LD ST, Vx
        /// Set sound timer = Vx.
        /// ST is set equal to the value of Vx.
        /// </summary>
        private void Inst_FX18(byte x)
        {
            this._ST = this.V[x];
        }


        /// <summary>
        /// Fx1E - ADD I, Vx
        /// Set I = I + Vx.
        /// The values of I and Vx are added, and the results are stored in I.
        /// </summary>
        private void Inst_FX1E(byte x)
        {
            I += this.V[x];
        }


        /// <summary>
        /// Fx29 - LD F, Vx
        /// Set I = location of sprite for digit Vx.
        /// The value of I is set to the location for the hexadecimal sprite corresponding to the value of Vx. See section 2.4, Display, for more information on the Chip-8 hexadecimal font.
        /// </summary>
        private void Inst_FX29(byte x)
        {
            I = (ushort)(V[x] * 5);
        }


        /// <summary>
        /// Fx33 - LD B, Vx
        /// Store BCD representation of Vx in memory locations I, I+1, and I+2.
        /// The interpreter takes the decimal value of Vx, and places the hundreds digit in memory at location in I, the tens digit at location I+1, and the ones digit at location I+2.
        /// </summary>
        private void Inst_FX33(byte x)
        {
            // TODO A étudier
            RAM[I] = (byte)(V[x] / 100);
            RAM[I + 1] = (byte)((V[x] % 100) / 10);
            RAM[I + 2] = (byte)((V[x] % 100) % 10);
        }


        /// <summary>
        /// Fx55 - LD [I], Vx
        /// Store registers V0 through Vx in memory starting at location I.
        /// The interpreter copies the values of registers V0 through Vx into memory, starting at the address in I.
        /// </summary>
        private void Inst_FX55(byte x)
        {
            for (var i = 0; i <= x; i++)
            {
                this.RAM[this.I + i] = this.V[i];
            }
        }


        /// <summary>
        /// Fx65 - LD Vx, [I]
        /// Read registers V0 through Vx from memory starting at location I.
        /// The interpreter reads values from memory starting at location I into registers V0 through Vx.
        /// </summary>
        private void Inst_FX65(byte x)
        {
            for (var i = 0; i <= x; i++)
            {
                this.V[i] = this.RAM[this.I + i];
            }
        }
    }
}
